using System;
using System.Linq;
using System.Threading;
using Moq;
using NUnit.Framework;
using System.Collections.Generic;

namespace PoolKit.Tests
{
	[TestFixture]
	public class GenericPoolTests
	{
        private class NewArraysCreatingStrategy : RepairStrategy<byte[]>
        {
            public override byte[] Repair(byte[] resource)
            {
                return new byte[resource.Length];
            }
        }

        private static List<byte[]> CreateListOfThreeByteArrays()
        {
            return new List<byte[]>(
                new[]{
				    new byte[8],
				    new byte[8],
				    new byte[8]});
        }

        private static List<TResource> GetAllObjectsFromPool<TResource>(GenericPool<TResource> pool)
            where TResource : class
        {
            var allPooledObjects = new List<TResource>();

            TResource objectFromPool;

            while (pool.TryLend(out objectFromPool))
            {
                allPooledObjects.Add(objectFromPool);
            }

            return allPooledObjects;
        }

		[Test]
		public void MakeSureLendReturnsAResourceFromThePool()
		{
		    List<byte[]> arrays = CreateListOfThreeByteArrays();

            using (var pool = GenericPoolCollaboratorsFactory.CreateDefaultGenericPool<byte[]>(arrays))
            {
                byte[] bytes = pool.Lend();

                CollectionAssert.Contains(arrays, bytes);
            }
		}

        [Test]
        public void MakeSureGiveUpReturnsAResourceFromThePool()
        {
            List<byte[]> arrays = CreateListOfThreeByteArrays();

            using (var pool = GenericPoolCollaboratorsFactory.CreateDefaultGenericPool<byte[]>(arrays))
            {
                byte[] buffer = pool.GiveUp();

                CollectionAssert.Contains(arrays, buffer);
            }
        }

        [Test]
        public void MakeSureYouCannotReturnToPoolSomeResourceThatHasBeenGivenUpByThePool()
        {
            List<byte[]> arrays = CreateListOfThreeByteArrays();

            using (var pool = GenericPoolCollaboratorsFactory.CreateDefaultGenericPool<byte[]>(arrays))
            {
                byte[] buffer = pool.GiveUp();

                Assert.Throws<CannotAcceptResourceException>(
                    () =>
                        {
                            pool.AcceptBack(buffer);
                        });

                Assert.Throws<CannotAcceptResourceException>(
                    () =>
                    {
                        pool.AcceptBackBroken(buffer);
                    });
            }
        }

        [Test]
        public void MakeSureYouCannotDoJackWithADisposedPool()
        {
            List<byte[]> arrays = CreateListOfThreeByteArrays();

            var pool = GenericPoolCollaboratorsFactory.CreateDefaultGenericPool(arrays);

            pool.Dispose();

            DisposablePatternTester.MakeSureDisposedObjectIsUnusable(pool);
        }
		
		[Test]
		public void MakeSureTryLendGivesAwayAResourceFromThePool()
		{
            List<byte[]> arrays = CreateListOfThreeByteArrays();

            using (var pool = GenericPoolCollaboratorsFactory.CreateDefaultGenericPool(arrays))
		    {
                byte[] bytes;
                Assert.IsTrue(pool.TryLend(out bytes));

                CollectionAssert.Contains(arrays, bytes);
		    }
		}

        [Test]
        [Timeout(10000)]
        public void MakeSureDisposingThePoolMakesItReleaseItsResources()
        {
            var arrayReferences = new List<WeakReference>();

            for (int i = 0; i < 3; i++)
            {
                arrayReferences.Add(new WeakReference(new byte[8]));
            }

            foreach (WeakReference arrayReference in arrayReferences)
            {
                Assert.IsTrue(arrayReference.IsAlive);
            }

            using (var pool = GenericPoolCollaboratorsFactory.CreateDefaultGenericPool(arrayReferences.Select(a => (byte[])a.Target)))
            {
                pool.AcceptBack(pool.Lend());
                pool.AcceptBack(pool.Lend());
            }

            while (!arrayReferences.All(x=>!x.IsAlive))
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();

                Thread.Sleep(200);
            }
        }

        [Test]
        [Timeout(10000)]
        public void MakeSureMakingAReplacementInThePoolMakesItReleaseTheReplacedResource()
        {
            List<byte[]> arrays = CreateListOfThreeByteArrays();

            var resourceCollection = new ResourceCollection<byte[]>(arrays);

            using (
                var pool = new GenericPool<byte[]>(
                    resourceCollection,
                    new Mock<ReusePreparationStrategy<byte[]>>().Object,
                        new NewArraysCreatingStrategy(),
                        GenericPoolCollaboratorsFactory.CreateDefaultCommandProcessor()))
            {
                var firstArrayReference = new WeakReference(pool.Lend());

                pool.AcceptBackBroken((byte[])firstArrayReference.Target);

                arrays.Clear();

                while (firstArrayReference.IsAlive)
                {
                    Thread.Sleep(200);

                    GC.Collect();
                    GC.WaitForPendingFinalizers();
                    GC.Collect();
                }
            }
        }
		
		[Test]		
		public void TestGetExhausted()
		{
            List<byte[]> arrays = CreateListOfThreeByteArrays();

            using (var pool = GenericPoolCollaboratorsFactory.CreateDefaultGenericPool(arrays))
            {
                pool.Lend();
                pool.Lend();
                pool.Lend();

                Assert.Throws<CannotLendResourceException>(
                    delegate
                    {
                        pool.Lend();
                    });
            }
		}
		
		[Test]
        [Timeout(10000)]
		public void MakeSureReturnPutsObjectsBackToPool()
		{
            List<byte[]> arrays = CreateListOfThreeByteArrays();

            var pool = GenericPoolCollaboratorsFactory.CreateDefaultGenericPool(arrays);

		    List<byte[]> gotFromPool = GetAllObjectsFromPool(pool);
			
			foreach (byte[] item in gotFromPool) {
				pool.AcceptBack(item);
			}

            var arraysReceivedFromPoolDuringIteration2 = new List<byte[]>();

            while (arraysReceivedFromPoolDuringIteration2.Count != arrays.Count)
            {
                byte[] byteArray;

                if (pool.TryLend(out byteArray))
                {
                    arraysReceivedFromPoolDuringIteration2.Add(byteArray);
                }

                Thread.Sleep(250);
            }

		    CollectionAssert.AreEquivalent(arrays, gotFromPool);
			CollectionAssert.AreEquivalent(gotFromPool, arraysReceivedFromPoolDuringIteration2);
		}

        [Test]
        public void MakeSureReusePreparationStrategyIsRunForEveryObjectReturnedToPool()
        {
            var array1 = new byte[8];
            var array2 = new byte[8];
            var array3 = new byte[8];

            var arrays = new List<byte[]> {array1, array2, array3};

            var reuseStrategyMock = new Mock<ReusePreparationStrategy<byte[]>>();

            var resourceCollection = new ResourceCollection<byte[]>(arrays);

            List<byte[]> allPooledObjects;
            var arraysThatCameThruReusePreparationStrategy = new List<byte[]>();

            using (var pool = new GenericPool<byte[]>(resourceCollection, reuseStrategyMock.Object, new Mock<RepairStrategy<byte[]>>().Object, GenericPoolCollaboratorsFactory.CreateDefaultCommandProcessor()))
            {
                allPooledObjects = GetAllObjectsFromPool(pool);

                reuseStrategyMock
                    .Setup(strategy => strategy.PrepareForReuse(It.IsAny<byte[]>()))
                    .Callback(
                        delegate(byte[] array)
                            {
                                lock (arraysThatCameThruReusePreparationStrategy)
                                {
                                    arraysThatCameThruReusePreparationStrategy.Add(array);
                                }
                            });

                foreach (byte[] bytes in allPooledObjects)
                {
                    pool.AcceptBack(bytes);
                }
            }

            lock (arraysThatCameThruReusePreparationStrategy)
            {
                CollectionAssert.AreEquivalent(allPooledObjects, arraysThatCameThruReusePreparationStrategy);
            }
        }

        [Test]
        public void MakeSureRepairStrategyIsRunForEveryBrokenObjectReturnedToPool()
        {
            var array1 = new byte[8];
            var array2 = new byte[8];
            var array3 = new byte[8];

            var arrays = new List<byte[]> { array1, array2, array3 };

            var reuseStrategyMock = new Mock<ReusePreparationStrategy<byte[]>>();

            var repairStrategyMock = new Mock<RepairStrategy<byte[]>>();

            var resourceCollection = new ResourceCollection<byte[]>(arrays);

            var arraysThatCameThruRepairStrategy = new List<byte[]>();
            List<byte[]> allPooledObjects;

            using (var pool = new GenericPool<byte[]>(resourceCollection, reuseStrategyMock.Object, repairStrategyMock.Object, GenericPoolCollaboratorsFactory.CreateDefaultCommandProcessor()))
            {
                allPooledObjects = GetAllObjectsFromPool(pool);

                repairStrategyMock.Setup(strategy => strategy.Repair(array1))
                    .Callback(
                        delegate(byte[] array)
                        {
                            lock (arraysThatCameThruRepairStrategy)
                            {
                                arraysThatCameThruRepairStrategy.Add(array);
                            }
                        }
                    )
                    .Returns(
                        delegate(byte[] incomingArray)
                        {
                            return incomingArray;
                        });

                foreach (byte[] bytes in allPooledObjects)
                {
                    pool.AcceptBackBroken(bytes);
                }
            }

            lock (arraysThatCameThruRepairStrategy)
            {
                CollectionAssert.AreEquivalent(allPooledObjects, arraysThatCameThruRepairStrategy);
            }
        }

        [Test]
        public void MakeSureYouCanDryPoolFromSeveralThreadsGettingNoErrors()
        {
            var arrays = new List<byte[]>();
            var arraysRetrieved = new List<byte[]>();

            const int numberOfByteArrays = 5000;
            const int numberOfThreads = 5;

            for (int i = 0; i < numberOfByteArrays; i++)
            {
                arrays.Add(new byte[8]);
            }

            using (var pool = GenericPoolCollaboratorsFactory.CreateDefaultGenericPool(arrays))
            {
                var allThreadsStartedEvent = new Barrier(numberOfThreads);
                var allThreadsQuitEvent = new CountdownEvent(numberOfThreads);

                Exception anyThreadException = null;

                for (int i = 0; i < numberOfThreads; i++)
                {
                    new Thread(delegate()
                    {
                        try
                        {
                            allThreadsStartedEvent.SignalAndWait();

                            byte[] array;

                            while (pool.TryLend(out array))
                            {
                                lock (arraysRetrieved)
                                {
                                    arraysRetrieved.Add(array);
                                }
                            }
                        }
                        catch (Exception exception)
                        {
                            anyThreadException = exception;
                        }
                        finally
                        {
                            allThreadsQuitEvent.Signal();
                        }
                    }).Start();
                }

                allThreadsQuitEvent.Wait();

                if (anyThreadException != null)
                {
                    throw anyThreadException;
                }

                CollectionAssert.AreEquivalent(arrays, arraysRetrieved);
            }
        }

        [Test]
        public void MakeSureYouCannotGetSameObjectToPoolMoreThanOnceDuringInitialization()
        {
            var array1 = new byte[8];
            var array2 = new byte[8];
            var array3 = new byte[8];

            var arrays = new List<byte[]> { array1, array2, array3, array1 };

            Assert.Throws<ArgumentException>(
                delegate()
                    {
                        GenericPoolCollaboratorsFactory.CreateDefaultGenericPool(arrays);
                    });
        }

        [Test]
        public void MakeSureYouCannotReturnObjectToPoolMoreThanOnce()
        {
            var array1 = new byte[8];

            var arrays = new List<byte[]> { array1 };

            using (var pool = GenericPoolCollaboratorsFactory.CreateDefaultGenericPool(arrays))
            {
                byte[] pooledArray = pool.Lend();

                pool.AcceptBack(pooledArray);

                Assert.Throws<CannotAcceptResourceException>(
                    delegate()
                    {
                        pool.AcceptBack(pooledArray);
                    });
            }
        }

        [Test]
        public void MakeSureYouCanReturnToPoolOnlyObjectsTakenFromPool()
        {
            var arrays = CreateListOfThreeByteArrays();

            using (var pool = GenericPoolCollaboratorsFactory.CreateDefaultGenericPool(arrays))
            {
                Assert.Throws<CannotAcceptResourceException>(
                    delegate()
                    {
                        pool.AcceptBack(new byte[54]);
                    });
            }
        }

        [Test]
        public void MakeSureThatReturningNewStringsOutOfRepairIsReflectedInResourceCollection()
        {
            var preparationStrategyMock = new Mock<ReusePreparationStrategy<string>>();
            var repairStrategyMock = new Mock<RepairStrategy<string>>(MockBehavior.Strict);

            repairStrategyMock.SetupGet(x => x.ThreadingContract).Returns(ThreadingContract.Multithreaded);
            repairStrategyMock
                .Setup(x => x.Repair(It.IsAny<string>()))
                .Returns((string resource) => (int.Parse(resource) + 3).ToString());

            var resourceCollection = new ResourceCollection<string>(new[] { "1", "2", "3" }, new FirstElementSelectionStrategy<string>());

            using (var pool = new GenericPool<string>(resourceCollection, preparationStrategyMock.Object, repairStrategyMock.Object, GenericPoolCollaboratorsFactory.CreateDefaultCommandProcessor()))
            {
                List<string> resources = resourceCollection.Drain();

                CollectionAssert.AreEquivalent(new[] { "1", "2", "3" }, resources);

                pool.AcceptBackBroken("1");
                pool.AcceptBackBroken("2");
                pool.AcceptBackBroken("3");

                // Sleep is necessary for the command processor to repair resources.
                // TODO: Implement a feature to wait for all commands to be executed.
                Thread.Sleep(3000);

                CollectionAssert.AreEquivalent(new[] { "4", "5", "6" }, resourceCollection.Drain());
            }

            
        }

        private static List<Mock<TMocked>> CreateListOfMocks<TMocked>(int count) where TMocked : class
        {
            var list = new List<Mock<TMocked>>();

            for (; count != 0; count--)
                list.Add(new Mock<TMocked>());

            return list;
        }

        [Test]
        public void MakeSureDisposableResourcesAreDisposed()
        {
            const int numberOfMocks = 5;
            var mocks = CreateListOfMocks<IDisposable>(numberOfMocks);
            Assert.AreEqual(numberOfMocks, mocks.Count);

            var resources = mocks.Select(x => x.Object);

            using (GenericPoolCollaboratorsFactory.CreateDefaultGenericPool(resources))
            {}

            foreach (var mock in mocks)
                mock.Verify(x => x.Dispose(), Times.Once());
        }

        [Test]
        public void MakeSureLentResourcesAreNotDisposedWhenDisposingThePool()
        {
            const int numberOfMocks = 5;
            var mocks = CreateListOfMocks<IDisposable>(numberOfMocks);
            Assert.AreEqual(numberOfMocks, mocks.Count);

            var resources = mocks.Select(x => x.Object);

            IDisposable lent = null;
            using (var pool = GenericPoolCollaboratorsFactory.CreateDefaultGenericPool(resources))
                lent = pool.Lend();
            
            Assert.IsNotNull(lent);

            foreach (var mock in mocks)
                if (ReferenceEquals(mock.Object, lent))
                    mock.Verify(x => x.Dispose(), Times.Never());
                else
                    mock.Verify(x => x.Dispose(), Times.Once());
        }
	}
}